﻿namespace Universe.MediaProbe
{
    using System;
    using System.Globalization;
    using System.Text;
    using System.Xml;
    using Utils;

    public class ProbeReportParser
    {
        static readonly CultureInfo EnUs = new CultureInfo("en-US");
        private static readonly decimal UNKNOWN_DECIMAL = 0m;
        private static readonly int UNKNOWN_INT = 0;
        
        public static ProbeReport Parse(byte[] xml)
        {
            if (xml == null)
                throw new ArgumentException("xml");

            Encoding utf8 = new UTF8Encoding(false);
            XmlDocument doc = null;
            Exception exParse = null;
            XmlUtils.SafeLoad(xml, utf8, out doc, out exParse);

            if (doc == null)
            {
                throw new InvalidOperationException("ffprobe valid xml report is expacted. Probably ffprobe is not supported by operation system. Please report to dev", exParse);
            }

            if (doc.DocumentElement.Name != "ffprobe")
                throw new ArgumentException("Xml Output of ffprobe is expected");

            ProbeReport ret = new ProbeReport();
            var elError = doc.DocumentElement.SelectSingleNode("error") as XmlElement;
            if (elError != null)
            {
                ret.Error = new ProbeReport.ErrorInfo
                {
                    Number = TrySimpleInt(elError, "@code"),
                    Description = TryString(elError, "@string"),
                };

                return ret;
            }

            var elFormat = doc.DocumentElement.SelectSingleNode("format") as XmlElement;
            if (elFormat != null)
            {
                ret.Format = TryString(elFormat, "@format_name");
                ret.FormatLongName = TryString(elFormat, "@format_long_name");
                ret.Duration = TrySimpleDecimal3(elFormat, "@duration");
                ret.BitRate = TrySimpleDecimal3(elFormat, "@bit_rate");
            }

            var listStreams = doc.DocumentElement.SelectNodes("streams/stream");
            foreach (XmlNode elStream in listStreams)
            {
                // common info 
                var streamType = TryString(elStream, "@codec_type");
                string codecName = TryString(elStream, "@codec_name");
                string codecLongName = TryString(elStream, "@codec_long_name");
                int? idStream = TryInt(elStream, "@index");

                if (codecName == null || codecLongName == null || !idStream.HasValue)
                    continue;

                if (streamType == "video")
                {
                    var avgFrameRate = TryFrameRate(TryString(elStream, "@avg_frame_rate"));
                    var frameRate = TryFrameRate(TryString(elStream, "@r_frame_rate"));
                    ret.Videos.Add(new ProbeReport.VideoInfo()
                    {
                        IdStream = idStream.Value,
                        Codec = codecName,
                        CodecLongName = codecLongName,
                        FrameRate = avgFrameRate == 0 ? frameRate : avgFrameRate,
                        Height = TrySimpleInt(elStream, "@height"),
                        Width = TrySimpleInt(elStream, "@width"),
                    });

                    
                }
                else if (streamType == "audio")
                {
                    ret.Audios.Add(new ProbeReport.AudioInfo
                    {
                        IdStream = idStream.Value,
                        Codec = codecName,
                        CodecLongName = codecLongName,
                        BitRate = TrySimpleInt(elStream, "@bit_rate"),
                        Channels = TrySimpleInt(elStream, "@channels"),
                        Language = TryString(elStream, "tag[@key='language']/@value"),
                        SampleRate = TrySimpleInt(elStream, "@sample_rate"),
                    });
                }
                else if (streamType == "subtitle")
                {
                    ret.Subs.Add(new ProbeReport.SubInfo
                    {
                        IdStream = idStream.Value,
                        Codec = codecName,
                        CodecLongName = codecLongName,
                        Language = TryString(elStream, "tag[@key='language']/@value"),
                    });
                }
            }

            return ret;
        }

        // raw: 23.297 | 24000/1001
        private static decimal TryFrameRate(string raw)
        {
            if (raw == null)
                return UNKNOWN_DECIMAL;

            decimal result;
            if (decimal.TryParse(raw, NumberStyles.Any | NumberStyles.AllowDecimalPoint, EnUs, out result))
                return result;

            var arr = raw.Split('/');
            if (arr.Length == 2)
            {
                decimal d1, d2;
                if (decimal.TryParse(arr[0], NumberStyles.Any, EnUs, out d1))
                    if (decimal.TryParse(arr[1], NumberStyles.Any, EnUs, out d2))
                        if (d2 != 0)
                            return CrossInfo.Round(d1/d2, 3);
            }

            return UNKNOWN_DECIMAL;
        }

        static string TryString(XmlNode node, string path)
        {
            var found = node.SelectSingleNode(path);
            if (found == null)
                return null;

            string ret = string.IsNullOrEmpty(found.InnerText) ? found.Value : found.InnerText;
            return string.IsNullOrEmpty(ret) ? null : ret;
        }

        static decimal TrySimpleDecimal(XmlNode node, string path)
        {
            var raw = TryString(node, path);
            decimal result;
            if (decimal.TryParse(raw, NumberStyles.Any | NumberStyles.AllowDecimalPoint, EnUs, out result))
                return result;

            return UNKNOWN_DECIMAL;
        }

        static int TrySimpleInt(XmlNode node, string path)
        {
            var ret = TryInt(node, path);
            return ret.HasValue ? ret.Value : UNKNOWN_INT;
        }

        static int? TryInt(XmlNode node, string path)
        {
            var raw = TryString(node, path);
            if (string.IsNullOrEmpty(raw))
                return null;

            int result;
            if (int.TryParse(raw, NumberStyles.Any, EnUs, out result))
                return result;

            return null;
        }

        
        static decimal TrySimpleDecimal3(XmlNode node, string path)
        {
            
            var raw_str = TryString(node, path);
            decimal raw_dec = TrySimpleDecimal(node, path);
            try
            {
                return CrossInfo.Round(raw_dec, 4);
            }
            catch (Exception ex)
            {
                throw new Exception("Rounding decimal [" + raw_dec + "] with string [" + raw_str + "] at path " + path + " failed", ex);
            }
        }


    }
}
